// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable StringLiteralTypo
// ReSharper disable UnusedParameter.Local

/*
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.IO;

using AM.Reporting.Utils;

using System.CodeDom;
using System.ComponentModel;
using System.Collections;

using AM.Reporting.CrossView;

using System.Windows.Forms;

#endregion

#nullable enable

namespace AM.Reporting.Data
{
    /// <summary>
    /// This class stores all report data items such as datasources, connections, relations, parameters,
    /// system variables.
    /// </summary>
    /// <remarks>
    /// You can access the report dictionary via <b>Report.Dictionary</b> property.
    /// </remarks>
    public class Dictionary : Base, IParent
    {
        #region Fields

        private ObjectCollection cachedAllObjects;
        private bool cacheAllObjects;

        #endregion

        #region Properties

        /// <summary>
        /// Gets a collection of connection objects available in a report.
        /// </summary>
        public ConnectionCollection Connections { get; }

        /// <summary>
        /// Gets a collection of datasources available in a report.
        /// </summary>
        /// <remarks>
        /// Usually you don't need to use this property. It contains only datasources
        /// registered using the <b>RegisterData</b> method. All other datasources are contained
        /// in connection objects and may be accessed via <see cref="Connections"/> property.
        /// </remarks>
        public DataSourceCollection DataSources { get; }

        /// <summary>
        /// Gets a collection of relations.
        /// </summary>
        public RelationCollection Relations { get; }

        /// <summary>
        /// Gets a collection of parameters.
        /// </summary>
        /// <remarks>
        /// Another way to access parameters is to use the <b>Report.Parameters</b> property
        /// which is actually a shortcut to this property. You also may use the <b>Report.GetParameter</b>
        /// and <b>Report.GetParameterValue</b> methods.
        /// </remarks>
        public ParameterCollection Parameters { get; }

        /// <summary>
        /// Gets a collection of system variables like Date, PageNofM etc.
        /// </summary>
        /// <remarks>
        /// Another way to access a system variable is to use the <b>Report.GetVariableValue</b> method.
        /// </remarks>
        public SystemVariables SystemVariables { get; }

        /// <summary>
        /// Gets a collection of totals.
        /// </summary>
        /// <remarks>
        /// Another way to get a total value is to use the <b>Report.GetTotalValue</b> method.
        /// </remarks>
        public TotalCollection Totals { get; }

        /// <summary>
        /// Gets a collection of cubesources available in a report.
        /// </summary>
        /// <remarks>
        /// Usually you don't need to use this property. It contains only cubesources
        /// registered using the <b>RegisterData</b> method.
        /// </remarks>
        public CubeSourceCollection CubeSources { get; }

        /// <summary>
        /// Gets a list of registered items.
        /// </summary>
        /// <remarks>
        /// This property is for internal use only.
        /// </remarks>
        public List<RegDataItem> RegisteredItems { get; }

        internal bool CacheAllObjects
        {
            get => cacheAllObjects;
            set
            {
                cacheAllObjects = value;
                if (cacheAllObjects)
                {
                    cachedAllObjects = base.AllObjects;
                }
                else
                {
                    cachedAllObjects = null;
                }
            }
        }

        ///<inheritdoc/>
        public new ObjectCollection AllObjects
        {
            get
            {
                if (cachedAllObjects != null)
                {
                    return cachedAllObjects;
                }

                return base.AllObjects;
            }
        }

        #endregion

        #region Private Methods

        private RegDataItem FindRegisteredItem (object data)
        {
            foreach (var item in RegisteredItems)
            {
                if (item.data == data)
                {
                    return item;
                }
            }

            return null;
        }

        private RegDataItem FindRegisteredItem (string name)
        {
            foreach (var item in RegisteredItems)
            {
                if (item.name == name)
                {
                    return item;
                }
            }

            return null;
        }

        internal void AddRegisteredItem (object data, string name)
        {
            if (FindRegisteredItem (data) == null)
            {
                RegisteredItems.Add (new RegDataItem (data, name));
            }
        }

        #endregion

        #region Public Methods

        /// <inheritdoc/>
        public override void Assign (Base source)
        {
            BaseAssign (source);
        }

        internal void RegisterDataSet (DataSet data, string referenceName, bool enabled)
        {
            var helper = new DictionaryHelper (this, AllObjects, Report.AllNamedObjects);
            helper.RegisterDataSet (data, referenceName, enabled);
        }

        internal void RegisterDataTable (DataTable table, string referenceName, bool enabled)
        {
            AddRegisteredItem (table, referenceName);

            if (FindDataComponent (referenceName) is TableDataSource source)
            {
                source.Reference = table;
                source.InitSchema();
                source.RefreshColumns (true);
            }
            else
            {
                // check tables inside connections. Are we trying to replace the connection table
                // with table provided by an application?
                source = FindByAlias (referenceName) as TableDataSource;

                // check "Data.TableName" case
                if (source == null && referenceName.StartsWith ("Data."))
                {
                    source = FindByAlias (referenceName.Remove (0, 5)) as TableDataSource;
                }

                if (source != null && (source.Connection != null || source.IgnoreConnection))
                {
                    source.IgnoreConnection = true;
                    source.Reference = table;
                    source.InitSchema();
                    source.RefreshColumns (true);
                }
                else
                {
                    source = new TableDataSource();
                    source.ReferenceName = referenceName;
                    source.Reference = table;
                    source.Name = CreateUniqueName (referenceName.Contains (".") ? table.TableName : referenceName);
                    source.Alias = CreateUniqueAlias (source.Alias);
                    source.Enabled = enabled;
                    source.InitSchema();
                    DataSources.Add (source);
                }
            }
        }

        /// <summary>
        /// Registers a DataView.
        /// </summary>
        /// <param name="view">The DataView to register.</param>
        /// <param name="referenceName">The name of the data object.</param>
        /// <param name="enabled">Determines wheter to enable the object or not.</param>
        /// <remarks>
        /// This method is for internal use only.
        /// </remarks>
        public void RegisterDataView (DataView view, string referenceName, bool enabled)
        {
            AddRegisteredItem (view, referenceName);

            if (FindDataComponent (referenceName) is ViewDataSource source)
            {
                source.Reference = view;
                source.InitSchema();
                source.RefreshColumns();
            }
            else
            {
                source = new ViewDataSource();
                source.ReferenceName = referenceName;
                source.Reference = view;
                source.Name = CreateUniqueName (referenceName);
                source.Alias = CreateUniqueAlias (source.Alias);
                source.Enabled = enabled;
                source.InitSchema();
                DataSources.Add (source);
            }
        }

        internal void RegisterDataRelation (DataRelation relation, string referenceName, bool enabled)
        {
            AddRegisteredItem (relation, referenceName);
            if (FindDataComponent (referenceName) != null)
            {
                return;
            }

            var rel = new Relation();
            rel.ReferenceName = referenceName;
            rel.Reference = relation;
            rel.Name = relation.RelationName;
            rel.Enabled = enabled;
            rel.ParentDataSource = FindDataTableSource (relation.ParentTable);
            rel.ChildDataSource = FindDataTableSource (relation.ChildTable);
            string[] parentColumns = new string[relation.ParentColumns.Length];
            string[] childColumns = new string[relation.ChildColumns.Length];
            for (var i = 0; i < relation.ParentColumns.Length; i++)
            {
                parentColumns[i] = relation.ParentColumns[i].Caption;
            }

            for (var i = 0; i < relation.ChildColumns.Length; i++)
            {
                childColumns[i] = relation.ChildColumns[i].Caption;
            }

            rel.ParentColumns = parentColumns;
            rel.ChildColumns = childColumns;
            Relations.Add (rel);
        }

        /// <summary>
        /// Registers a business object.
        /// </summary>
        /// <param name="data">The business object.</param>
        /// <param name="referenceName">The name of the object.</param>
        /// <param name="maxNestingLevel">Maximum level of data nesting.</param>
        /// <param name="enabled">Determines wheter to enable the object or not.</param>
        /// <remarks>
        /// This method is for internal use only.
        /// </remarks>
        public void RegisterBusinessObject (IEnumerable data, string referenceName, int maxNestingLevel, bool enabled)
        {
            AddRegisteredItem (data, referenceName);

            var dataType = data.GetType();
            if (data is BindingSource bindingSource)
            {
                if (bindingSource.DataSource is Type type)
                {
                    dataType = type;
                }
                else
                {
                    dataType = bindingSource.DataSource.GetType();
                }
            }

            var converter = new BusinessObjectConverter (this);
            if (FindDataComponent (referenceName) is BusinessObjectDataSource source)
            {
                source.Reference = data;
                source.DataType = dataType;
                converter.UpdateExistingObjects (source, maxNestingLevel);
            }
            else
            {
                source = new BusinessObjectDataSource();
                source.ReferenceName = referenceName;
                source.Reference = data;
                source.DataType = dataType;
                source.Name = CreateUniqueName (referenceName);
                source.Alias = CreateUniqueAlias (source.Alias);
                source.Enabled = enabled;
                DataSources.Add (source);

                converter.CreateInitialObjects (source, maxNestingLevel);
            }
        }

        /// <summary>
        /// Registers a CubeLink.
        /// </summary>
        /// <param name="cubeLink">The CubeLink to register.</param>
        /// <param name="referenceName">The name of the data object.</param>
        /// <param name="enabled">Determines wheter to enable the object or not.</param>
        /// <remarks>
        /// This method is for internal use only.
        /// </remarks>
        public void RegisterCubeLink (IBaseCubeLink cubeLink, string referenceName, bool enabled)
        {
            AddRegisteredItem (cubeLink, referenceName);

            if (FindDataComponent (referenceName) is CubeSourceBase source)
            {
                source.Reference = cubeLink;
            }
            else
            {
                source = new SliceCubeSource();
                source.ReferenceName = referenceName;
                source.Reference = cubeLink;
                source.Name = CreateUniqueName (referenceName);
                source.Alias = CreateUniqueAlias (source.Alias);
                source.Enabled = enabled;
                CubeSources.Add (source);
            }
        }

        /// <summary>
        /// Registers a data object.
        /// </summary>
        /// <param name="data">The object to register.</param>
        /// <param name="name">The name of the object.</param>
        /// <param name="enabled">Determines wheter to enable the object or not.</param>
        /// <remarks>
        /// This method is for internal use only.
        /// </remarks>
        public void RegisterData (object data, string name, bool enabled)
        {
            if (data is DataSet set)
            {
                RegisterDataSet (set, name, enabled);
            }
            else if (data is DataTable table)
            {
                RegisterDataTable (table, name, enabled);
            }
            else if (data is DataView view)
            {
                RegisterDataView (view, name, enabled);
            }
            else if (data is DataRelation relation)
            {
                RegisterDataRelation (relation, name, enabled);
            }
            else if (data is IEnumerable enumerable)
            {
                RegisterBusinessObject (enumerable, name, 1, enabled);
            }
            else if (data is IBaseCubeLink link)
            {
                RegisterCubeLink (link, name, enabled);
            }
        }

        /// <summary>
        /// Unregisters the previously registered data.
        /// </summary>
        /// <param name="data">The application data.</param>
        public void UnregisterData (object data)
        {
            UnregisterData (data, "Data");
        }

        /// <summary>
        /// Unregisters the previously registered data.
        /// </summary>
        /// <param name="data">The application data.</param>
        /// <param name="name">The name of the data.</param>
        /// <remarks>
        /// You must specify the same <b>data</b> and <b>name</b> as when you call <b>RegisterData</b>.
        /// </remarks>
        public void UnregisterData (object data, string name)
        {
            for (var i = 0; i < RegisteredItems.Count; i++)
            {
                var item = RegisteredItems[i];
                if (item.name == name)
                {
                    RegisteredItems.RemoveAt (i);
                    break;
                }
            }

            var comp = FindDataComponent (name);
            if (comp != null)
            {
                comp.Dispose();
            }

            if (data is DataSet set)
            {
                foreach (DataTable table in set.Tables)
                {
                    UnregisterData (table, name + "." + table.TableName);
                }

                foreach (DataRelation relation in set.Relations)
                {
                    UnregisterData (relation, name + "." + relation.RelationName);
                }
            }
        }

        /// <summary>
        /// Re-registers the data registered before.
        /// </summary>
        /// <remarks>
        /// This method is for internal use only.
        /// </remarks>
        public void ReRegisterData()
        {
            ReRegisterData (this);
        }

        /// <summary>
        /// Re-registers the data registered before.
        /// </summary>
        /// <param name="dictionary"></param>
        public void ReRegisterData (Dictionary dictionary)
        {
            // re-register all data items. It is needed after load or "new report" operations
            if (RegisteredItems.Count > 0)
            {
                var helper = new DictionaryHelper (dictionary, dictionary.AllObjects,
                    dictionary.Report.AllNamedObjects);
                helper.ReRegisterData (RegisteredItems, false);
            }
        }

        /// <summary>
        /// Clears all registered data.
        /// </summary>
        public void ClearRegisteredData()
        {
            RegisteredItems.Clear();
        }

        /// <summary>
        /// Enables or disables relations between data tables.
        /// </summary>
        /// <remarks>
        /// Call this method if you create master-detail report from code. This method enables
        /// relation between two data tables which <b>Enabled</b> flag is set to <b>true</b>. Relations
        /// whose parent and child tables are disabled, gets disabled too.
        /// </remarks>
        public void UpdateRelations()
        {
            foreach (Relation relation in Relations)
            {
                relation.Enabled = relation is { ParentDataSource: { Enabled: true }, ChildDataSource: { Enabled: true } };
            }
        }

        /// <summary>
        /// Creates unique name for data item such as connection, datasource, relation, parameter or total.
        /// </summary>
        /// <param name="name">The base name.</param>
        /// <returns>The new unique name.</returns>
        /// <remarks>
        /// Use this method to create unique name of the data item. It is necessary when you create new
        /// items in code to avoid conflicts with existing report items.
        /// <example>This example show how to add a new parameter:
        /// <code>
        /// Report report1;
        /// Parameter par = new Parameter();
        /// par.Name = report1.Dictionary.CreateUniqueName("Parameter");
        /// report1.Parameters.Add(par);
        /// </code>
        /// </example>
        /// </remarks>
        public string CreateUniqueName (string name)
        {
            var baseName = name;
            var i = 1;
            while (FindByName (name) != null || Report.FindObject (name) != null)
            {
                name = baseName + i.ToString();
                i++;
            }

            return name;
        }

        /// <summary>
        /// Creates unique alias for data item such as connection, datasource or relation.
        /// </summary>
        /// <param name="alias">The base alias.</param>
        /// <returns>The new unique alias.</returns>
        /// <remarks>
        /// Use this method to create unique alias of the data item. It is necessary when you create new
        /// items in code to avoid conflicts with existing report items.
        /// <example>This example show how to add a new table:
        /// <code>
        /// Report report1;
        /// DataConnectionBase conn = report1.Dictionary.Connections.FindByName("Connection1");
        /// TableDataSource table = new TableDataSource();
        /// table.TableName = "Employees";
        /// table.Name = report1.Dictionary.CreateUniqueName("EmployeesTable");
        /// table.Alias = report1.Dictionary.CreateUniqueAlias("Employees");
        /// conn.Tables.Add(table);
        /// </code>
        /// </example>
        /// </remarks>
        public string CreateUniqueAlias (string alias)
        {
            var baseAlias = alias;
            var i = 1;
            while (FindByAlias (alias) != null)
            {
                alias = baseAlias + i.ToString();
                i++;
            }

            return alias;
        }

        /// <summary>
        /// Finds a data item such as connection, datasource, relation, parameter or total by its name.
        /// </summary>
        /// <param name="name">The item's name.</param>
        /// <returns>The data item if found; otherwise, <b>null</b>.</returns>
        public Base FindByName (string name)
        {
            foreach (Base c in AllObjects)
            {
                if (c is DataConnectionBase or DataSourceBase or Relation ||
                    (c is Parameter && c.Parent == this) || c is Total or CubeSourceBase)
                {
                    // check complete match or match without case sensitivity
                    if (name == c.Name || name.ToLower() == c.Name.ToLower())
                    {
                        return c;
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Finds a data item such as connection, datasource or relation by its alias.
        /// </summary>
        /// <param name="alias">The item's alias.</param>
        /// <returns>The data item if found; otherwise, <b>null</b>.</returns>
        public DataComponentBase FindByAlias (string alias)
        {
            foreach (Base c in AllObjects)
            {
                if (c is DataConnectionBase or Relation)
                {
                    // check complete match or match without case sensitivity
                    if (alias == (c as DataComponentBase).Alias ||
                        alias.ToLower() == (c as DataComponentBase).Alias.ToLower())
                    {
                        return c as DataComponentBase;
                    }
                }
                else if (c is DataSourceBase @base)
                {
                    // check complete match or match without case sensitivity
                    if (alias == @base.FullName ||
                        alias.ToLower() == @base.FullName.ToLower())
                    {
                        return @base;
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Finds a datasource that matches the specified DataTable.
        /// </summary>
        /// <param name="table">The <b>DataTable</b> object to check.</param>
        /// <returns>The <b>DataSourceBase</b> object if found.</returns>
        /// <remarks>
        /// This method is for internal use only.
        /// </remarks>
        public DataSourceBase FindDataTableSource (DataTable table)
        {
            foreach (DataSourceBase c in DataSources)
            {
                if (c is TableDataSource && c.Reference == table)
                {
                    return c;
                }
            }

            return null;
        }

        /// <summary>
        /// Finds a data component that matches the specified reference name.
        /// </summary>
        /// <param name="referenceName">The name to check.</param>
        /// <returns>The <b>DataComponentBase</b> object if found.</returns>
        /// <remarks>
        /// This method is for internal use only.
        /// </remarks>
        public DataComponentBase FindDataComponent (string referenceName)
        {
            foreach (Base c in AllObjects)
            {
                if (c is DataComponentBase data && data.ReferenceName == referenceName)
                {
                    return data;
                }
            }

            return null;
        }

        /// <inheritdoc/>
        public override void Serialize (ReportWriter writer)
        {
            writer.ItemName = ClassName;
            var childObjects = ChildObjects;
            foreach (Base c in childObjects)
            {
                if (c is Parameter or Total or CubeSourceBase or DataComponentBase { Enabled: true })
                {
                    writer.Write (c);
                }
            }
        }

        /// <inheritdoc/>
        public override void Deserialize (ReportReader reader)
        {
            base.Deserialize (reader);
            ReRegisterData();
        }

        /// <summary>
        /// Saves the dictionary to a stream.
        /// </summary>
        /// <param name="stream">Stream to save to.</param>
        public void Save (Stream stream)
        {
            using (var writer = new ReportWriter())
            {
                writer.Write (this);
                writer.Save (stream);
            }
        }

        /// <summary>
        /// Saves the dictionary to a file.
        /// </summary>
        /// <param name="fileName">The name of a file to save to.</param>
        public void Save (string fileName)
        {
            using (var f = new FileStream (fileName, FileMode.Create))
            {
                Save (f);
            }
        }

        /// <summary>
        /// Loads the dictionary from a stream.
        /// </summary>
        /// <param name="stream">The stream to load from.</param>
        public void Load (Stream stream)
        {
            Clear();
            using (var reader = new ReportReader (Report))
            {
                reader.Load (stream);
                reader.Read (this);
            }
        }

        /// <summary>
        /// Loads the dictionary from a file.
        /// </summary>
        /// <param name="fileName">The name of a file to load from.</param>
        public void Load (string fileName)
        {
            using (var f = new FileStream (fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                Load (f);
            }
        }

        /// <summary>
        /// Merges this dictionary with another <b>Dictionary</b>.
        /// </summary>
        /// <param name="source">Another dictionary to merge the data from.</param>
        public void Merge (Dictionary source)
        {
            // Report object is needed to handle save/load of dictionary correctly.
            // Some dictionary objects (such as relations) may contain references to other objects.
            // In order to clone them correctly, we need a parent Report object, because
            // reader uses it to fixup references.

            using (var cloneReport = new Report())
            {
                var clone = cloneReport.Dictionary;
                clone.AssignAll (source, true);

                foreach (Base c in clone.ChildObjects)
                {
                    var my = FindByName (c.Name);
                    if (my != null)
                    {
                        my.Dispose();
                    }

                    c.Parent = this;
                }

                source.ReRegisterData (this);
                ReRegisterData();
            }
        }

        #endregion

        #region IParent Members

        /// <inheritdoc/>
        public bool CanContain (Base child)
        {
            return child is DataConnectionBase or DataSourceBase or Relation or Parameter or Total or CubeSourceBase;
        }

        /// <inheritdoc/>
        public void GetChildObjects (ObjectCollection list)
        {
            foreach (DataConnectionBase c in Connections)
            {
                list.Add (c);
            }

            foreach (DataSourceBase c in DataSources)
            {
                list.Add (c);
            }

            foreach (Relation c in Relations)
            {
                list.Add (c);
            }

            foreach (Parameter c in Parameters)
            {
                list.Add (c);
            }

            foreach (Total c in Totals)
            {
                list.Add (c);
            }

            foreach (CubeSourceBase c in CubeSources)
            {
                list.Add (c);
            }
        }

        /// <inheritdoc/>
        public void AddChild (Base child)
        {
            if (child is DataConnectionBase @base)
            {
                Connections.Add (@base);
            }
            else if (child is DataSourceBase sourceBase)
            {
                DataSources.Add (sourceBase);
            }
            else if (child is Relation relation)
            {
                Relations.Add (relation);
            }
            else if (child is Parameter parameter)
            {
                Parameters.Add (parameter);
            }
            else if (child is Total total)
            {
                Totals.Add (total);
            }
            else if (child is CubeSourceBase cubeSourceBase)
            {
                CubeSources.Add (cubeSourceBase);
            }
        }

        /// <inheritdoc/>
        public void RemoveChild (Base child)
        {
            if (child is DataConnectionBase @base)
            {
                Connections.Remove (@base);
            }
            else if (child is DataSourceBase sourceBase)
            {
                DataSources.Remove (sourceBase);
            }
            else if (child is Relation relation)
            {
                Relations.Remove (relation);
            }
            else if (child is Parameter parameter)
            {
                Parameters.Remove (parameter);
            }
            else if (child is Total total)
            {
                Totals.Remove (total);
            }
            else if (child is CubeSourceBase cubeSourceBase)
            {
                CubeSources.Remove (cubeSourceBase);
            }
        }

        /// <inheritdoc/>
        public int GetChildOrder (Base child)
        {
            return 0;
        }

        /// <inheritdoc/>
        public void SetChildOrder (Base child, int order)
        {
            // do nothing
        }

        /// <inheritdoc/>
        public void UpdateLayout (float dx, float dy)
        {
            // do nothing
        }

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="Dictionary"/> class with default settings.
        /// </summary>
        public Dictionary()
        {
            Connections = new ConnectionCollection (this);
            DataSources = new DataSourceCollection (this);
            Relations = new RelationCollection (this);
            Parameters = new ParameterCollection (this);
            SystemVariables = new SystemVariables (this);
            Totals = new TotalCollection (this);
            CubeSources = new CubeSourceCollection (this);
            RegisteredItems = new List<RegDataItem>();
        }

        /// <summary>
        /// Represents the item registered in a dictionary.
        /// </summary>
        public class RegDataItem
        {
            /// <summary>
            /// Gets the item data.
            /// </summary>
            public object data;

            /// <summary>
            /// Gets the item name.
            /// </summary>
            public string name;

            internal RegDataItem (object data, string name)
            {
                this.data = data;
                this.name = name;
            }
        }
    }
}
